/** @file f_zipstream.cpp
	@brief A Zipped Filestream */

#include "file/f_zipstream.h"

using namespace G3;

//==============================================================================
// Constructors & Destructors
//==============================================================================
G3::ZIPSTREAM::ZIPSTREAM (STREAM *stream):
    ZSTREAM (stream) {

	zStream.zalloc = Z_NULL;
	zStream.zfree = Z_NULL;
	zStream.opaque = Z_NULL;
}

G3::ZIPSTREAM::~ZIPSTREAM () {
    if (IsOpen (0))
        Close ();
}

//==============================================================================
// Opens a ZFILESTREAM either for Deflating or Inflating data..
//
//		byte OpenFlags				Flags for the stream
//				ZSTREAM_DEFLATE		Opens for deflating (compressed buffering)
//				ZSTREAM_INFLATE		Opens for inflating (decompressed reading)
//
//		int Level					Compression level
//				-1					Default, level 6
//				0					No compression at all
//				1 ... 9				Lowest ... Hightest compression
//
//	Returns true if successful
//==============================================================================
bool G3::ZIPSTREAM::Open (byte openFlags, int level) {
    Assert (bStream, "ZIPSTREAM::Open: bStream member is a NULL pointer..\n");
	Assert (level >= -1 && level <= 9, "ZIPSTREAM::Open: Invalid compression level");

	int ret = 0;

	// Already open?
	if ((flags & ZSTREAM_OPEN) != 0) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::Open: Stream already open.\n");
		return false;
	}

	compressionLevel = (char) level;

	// Are we asked to deflate or inflate?
	if ((openFlags & ZSTREAM_DEFLATE) != 0) {
		ret = deflateInit (&zStream, (int) compressionLevel);
		if (ret != Z_OK) {
			if (log)
				log->Report ("ERROR! ZIPSTREAM::Open: ZLIB::deflateInit failed with code: %d\n", ret);
			return false;
		}

		flags |= ZSTREAM_DEFLATE;
	} else {
		zStream.avail_in = 0;
		zStream.next_in = Z_NULL;

		ret = inflateInit (&zStream);
		if (ret != Z_OK) {
			if (log)
				log->Report ("ERROR! ZIPSTREAM::Open: ZLIB::inflateInit failed with code: %d\n", ret);
			return false;
		}

		flags |= ZSTREAM_INFLATE;
	}

	// Mark our stream open
	flags |= ZSTREAM_OPEN;

	return true;
}

//==============================================================================
// Closes the ZFILESTREAM
//==============================================================================
void G3::ZIPSTREAM::Close () {
	if ((flags & ZSTREAM_OPEN) != 0) {
		if ((flags & ZSTREAM_DEFLATE) != 0)
			deflateEnd (&zStream);
		if ((flags & ZSTREAM_INFLATE) != 0)
			inflateEnd (&zStream);
	}
	Disable (ZSTREAM_OPEN);
}

//==============================================================================
// Compresses data and adds it to the stream buffer
//
//		void *Pointer			Pointer to a memory block from where to compress
//
//		long Size				Number of bytes to compress
//
//	NOTE:: To end the compression, either Pointer has to become NULL or Size 0
//
//	Returns true on success
//==============================================================================
bool G3::ZIPSTREAM::Deflate (byte *pointer, long size) {
    Assert (bStream, "ZIPSTREAM::Deflate: bStream member is a NULL pointer..\n");

	if (!IsOpen (ZSTREAM_DEFLATE)) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::Deflate: Stream is not opened for deflating (Flags: 0x%X)..\n", flags);
		return false;
	}

	byte *data2 = (byte *) malloc (outputChunkSize + 1);
	int flush = Z_NO_FLUSH;
	int ret = 0;

	if (data2 == NULL) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::Deflate: Not enough memory for deflating (at least %ld more bytes needed)..\n", outputChunkSize + 1);
		return false;
	}

	// Is this the last chunk?
	if (pointer == NULL || size <= 0) {
		flush = Z_FINISH;

		zStream.avail_in = 0;
		zStream.next_in = Z_NULL;
	} else {
		zStream.avail_in = size;
		zStream.next_in = pointer;
	}

	// Run the deflate function until its job is complete
	while (true) {
		zStream.avail_out = outputChunkSize;
		zStream.next_out = data2;

		// Attempt to deflate
		ret = deflate (&zStream, flush);

		// Did deflate produce any output?
		if (outputChunkSize - zStream.avail_out > 0) {
			// Buffer the freshly compressed data
			bStream->Buffer (data2, outputChunkSize - zStream.avail_out);
		}

		if (ret == Z_STREAM_ERROR) {
			if (log)
				log->Report ("ERROR! ZIPSTREAM::Deflate: ZLIB:deflate threw Z_STREAM_ERROR..\n");

            free (data2);

			deflateEnd (&zStream);
			flags &= (~ZSTREAM_OPEN);
			return false;
		}

        // Loop until everything has been deflated
        if (flush == Z_FINISH && ret == Z_STREAM_END)
            break;
        else if (zStream.avail_out != 0)
            break;
	}

	free (data2);

	return true;
}

//==============================================================================
// Reads data from the stream and decompresses it
//
//		void *Pointer			Pointer to a memory block where the data is stored
//
//		long Size				Number of bytes saved at the Pointer
//
//		bool FlushBuffer		Is the stream buffer flushed after inflating?
//				true			The part of the buffer that is already read, is freed
//				false			The data already read is still stored
//
//	NOTE:: FlushBuffer has no effect when STREAM_ACCUM_BUF flag is set
//
//	Returns the number of bytes stored at Pointer
//==============================================================================
long G3::ZIPSTREAM::Inflate (byte *pointer, long size) {
    Assert (bStream, "ZIPSTREAM::Inflate: bStream member is a NULL pointer..\n");
	Assert (pointer, "ZIPSTREAM::Inflate: Pointer argument is a NULL pointer");
	Assert (size > 0, "ZIPSTREAM::Inflate: Size argument is too small (<= 0)");

	if (!IsOpen (ZSTREAM_INFLATE)) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::Inflate: Stream is not opened for inflating (Flags: %d)..\n", flags);
		return -1;
	}

	int ret = 0;
	long size2 = size;

	// Is someone flushing / buffering already?
	if (bStream->bufferReadLocked)
		return -1;

	// Make sure nobody is moving our caret during the read
	bStream->caretLocked = true;
	// Make sure the buffer data pointer stays constant during our read
	bStream->bufferWriteLocked = true;

	STREAM::CHUNK chunk = bStream->GetChunk ();

	zStream.avail_in = chunk.chunkSize;//BufferedSize - CaretPosition + BufferPosition;
	zStream.next_in = chunk.chunkPointer;//(byte *) Data + CaretPosition - BufferPosition;
	zStream.avail_out = size;
	zStream.next_out = pointer;

	// Attempt to inflate
	ret = inflate (&zStream, Z_NO_FLUSH);
	if (ret != Z_OK) {
		switch (ret) {
			case Z_STREAM_ERROR:
				if (log)
					log->Report ("ERROR! ZIPSTREAM::Inflate: ZLIB:inflate threw Z_STREAM_ERROR..\n");
				break;

			case Z_NEED_DICT:
				if (log)
					log->Report ("ERROR! ZIPSTREAM::Inflate: ZLIB:inflate: Unknown fileformat..\n");
				break;

			case Z_MEM_ERROR:
				if (log)
					log->Report ("ERROR! ZIPSTREAM::Inflate: ZLIB:inflate: Not enough memory for inflating..\n");
				break;

			case Z_DATA_ERROR:
				if (log)
					log->Report ("ERROR! ZIPSTREAM::Inflate: ZLIB:inflate: Compressed Data is invalid..\n");
				break;
			// Yes, I know this is a bad coding style..
			// At least it does not require any extra memory and I don't have to do
			// a dozen of 'if's - it is just the fastest, cleanest way to break out of this clause
			default: goto zfilestream_inflate1;
		}

		// Free the caret to other threads
		bStream->caretLocked = false;
		// Free the buffer to other threads
		bStream->bufferWriteLocked = false;

		inflateEnd (&zStream);
		flags &= (~ZSTREAM_OPEN);
		return -1;
	}

// And here we go again with this spaghetti coding style..
zfilestream_inflate1:

    bStream->Seek (chunk.chunkSize, STREAM::STREAM_MOVE);
	//caretPosition += BufferedSize;

	// Free the caret to other threads
	bStream->caretLocked = false;
	// Free the buffer to other threads
	bStream->bufferWriteLocked = false;

	// Number of bytes stored at Pointer
	return size - zStream.avail_out;
}

//==============================================================================
// Decompresses data into the stream
//
//		void *Pointer			Pointer to a memory block where the data is read from
//
//		long Size				Number of bytes read from the Pointer
//
//	Returns the number of bytes decompressed into the stream
//==============================================================================
long G3::ZIPSTREAM::InflateIn (byte *pointer, long size) {
    Assert (bStream, "ZIPSTREAM::InflateIn: bStream member is a NULL pointer..\n");

	if (!IsOpen (ZSTREAM_INFLATE)) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::InflateIn: Stream is not opened for deflating (Flags: %d)..\n", flags);
		return false;
	}

	byte *data2 = (byte *) malloc (outputChunkSize + 1);
	int flush = Z_NO_FLUSH;
	long ret = 0;

	if (data2 == NULL) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::InflateIn: Not enough memory for deflating (at least %ld more bytes needed)..\n", outputChunkSize + 1);
		return false;
	}

	// Is this the last chunk?
	if (pointer == NULL || size <= 0) {
		flush = Z_FINISH;

		zStream.avail_in = 0;
		zStream.next_in = Z_NULL;
	} else {
		zStream.avail_in = size;
		zStream.next_in = pointer;
	}

	// Run the inflate function until its job is complete
	while (true) {
		zStream.avail_out = outputChunkSize;
		zStream.next_out = data2;

		// Attempt to inflate
		ret = inflate (&zStream, flush);

		// Did inflate produce any output?
		if (outputChunkSize - zStream.avail_out > 0) {
			// Buffer the freshly decompressed data
			bStream->Buffer (data2, outputChunkSize - zStream.avail_out);
		}

		if (ret != Z_OK) {
			switch (ret) {
				case Z_STREAM_ERROR:
					if (log)
						log->Report ("ERROR! ZIPSTREAM::InflateIn: ZLIB:inflate threw Z_STREAM_ERROR..\n");
					break;

				case Z_NEED_DICT:
					if (log)
						log->Report ("ERROR! ZIPSTREAM::InflateIn: ZLIB:inflate: Unknown fileformat..\n");
					break;

				case Z_MEM_ERROR:
					if (log)
						log->Report ("ERROR! ZIPSTREAM::InflateIn: ZLIB:inflate: Not enough memory for inflating..\n");
					break;

				case Z_DATA_ERROR:
					if (log)
						log->Report ("ERROR! ZIPSTREAM::InflateIn: ZLIB:inflate: Compressed Data is invalid..\n");
					break;
			}
		}

        // Loop until everything has been inflated
		if (ret == Z_STREAM_END){
		    printf ("<<< Stream End >>>\n");
            break;
        } else if (zStream.avail_in == 0)
            break;
	}

	inflateEnd (&zStream);
	flags &= (~ZSTREAM_OPEN);

	free (data2);
	data2 = NULL;

	return size - zStream.avail_out;
}

//==============================================================================
// Compresses data from the stream
//
//		void *Pointer			Pointer to a memory block where to compress
//
//		long Size				Number of bytes to compress
//
//	NOTE:: To end the compression, Size has to become 0
//
//	Returns true on success
//==============================================================================
long G3::ZIPSTREAM::DeflateOut (byte *pointer, long size) {
    Assert (bStream, "ZIPSTREAM::DeflateOut: bStream member is a NULL pointer..\n");
	Assert (pointer, "ZIPSTREAM::DeflateOut: Pointer argument is a NULL pointer");
	Assert (size > 0, "ZIPSTREAM::DeflateOut: Size argument is too small (<= 0)");

	if (!IsOpen (ZSTREAM_DEFLATE)) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::DeflateOut: Stream is not opened for inflating (Flags: 0x%X)..\n", flags);
		return 0;
	}

	long written = 0, read = 0;
	int flush = Z_NO_FLUSH;

	// Is someone flushing / buffering already?
	if (bStream->bufferReadLocked) {
	    if (log)
            log->Report ("WARNING! ZIPSTREAM::DeflateOut: Buffer reading is locked..\n");
		return 0;
	}

	// Make sure nobody is moving our caret during the read
	bStream->caretLocked = true;
	// Make sure the buffer data pointer stays constant during our read
	bStream->bufferWriteLocked = true;

	STREAM::CHUNK chunk;

    zStream.avail_out = size;
    zStream.next_out = pointer;

	while (true) {
	    chunk = bStream->GetChunk ();

        read += chunk.chunkSize;

        // No more chunks? Finish it then
        if (chunk.chunkSize == 0) {
            chunk.chunkPointer = Z_NULL;
            flush = Z_FINISH;
        }

        zStream.avail_in = chunk.chunkSize;
        zStream.next_in = chunk.chunkPointer;

        // Attempt to deflate
        if (deflate (&zStream, flush) == Z_STREAM_ERROR) {
            if (log)
                log->Report ("ERROR! ZIPSTREAM::DeflateOut: Failed to deflate..\n");

            // Free the caret to other threads
            bStream->caretLocked = false;
            // Free the buffer to other threads
            bStream->bufferWriteLocked = false;

            deflateEnd (&zStream);
            flags &= (~ZSTREAM_OPEN);

            return written + size - zStream.avail_out;
        }

        log->Report ("Chunk %d: %d bytes written, %d/%d more to come\n", chunk.chunkSize, size - zStream.avail_out, zStream.avail_out, size);

        // How many bytes were written?
        written += size - zStream.avail_out;
        // Prepare for the next chunk
        //bStream->Seek (chunk.chunkSize, STREAM::STREAM_MOVE);

        if (chunk.chunkSize == 0) {
            printf ("Break;\n");
            break;
        }
	}

	// Free the caret to other threads
	bStream->caretLocked = false;
	// Free the buffer to other threads
	bStream->bufferWriteLocked = false;

	// Number of bytes stored at Pointer
	return written;
}

//==============================================================================
// Reads data from a file and decompresses it
//
//		const char *Filename	Name of the file to buffer
//
//		long Size				Number of bytes to buffer
//				-1, 0			Buffers the whole file into the stream
//
//		long Position			Where in the file should we start reading
//
//	Returns true on success
//==============================================================================
long G3::ZIPSTREAM::InflateFromFile (const string &filename, long size, long position) {
	long fsize = 0;
	long chunkSize = size;
	long ret = 0;
	byte *data2 = NULL;

    Assert (bStream, "ZIPSTREAM::InflateFromFile: bStream member is a NULL pointer..\n");
	Assert (!filename.empty (), "ZIPSTREAM::InflateFromFile: Filename argument is a NULL pointer");

	fsize = GetFileSize (filename);

	Assert (position >= 0 && position < fsize, "ZIPSTREAM::InflateFromFile: Position argument is invalid (< 0 || >= Filesize)");

	// Make sure the ChunkSize is valid
	if (size <= 0)
		chunkSize = fsize - position;
	if (size > fsize)
		chunkSize = fsize;

	// Try to open the file for binary reading
	FILE *fi = fopen (filename.c_str (), "rb");
	if (!fi) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::InflateFromFile: Failed to open the file \"%s\"..\n", filename.c_str ());
		return 0;
	}

	// Set the filename member
	bStream->fileName = filename;
	// Set the start of the file
	//SetFileStart ();

	// Make a temporary buffer
	data2 = new byte [chunkSize + 1];

	// Seek to the asked Position
	fseek (fi, position, SEEK_SET);

	// Read the file
	ret = fread (data2, 1, chunkSize, fi);

	// Buffer the stream from our temporary buffer
	InflateIn (data2, ret);

	// Free our temporary buffer
	delete [] data2;

	fclose (fi);

	// Seek to the beginning of the file
    bStream->Seek (0, G3::STREAM::STREAM_BEGINNING);

	// Set the size of the buffered file
	//SetFileSize (ret);

	return ret;
}

//==============================================================================
// Reads data from the stream and compresses it into a file
//
//		const char *Filename	Name of the file to store the data in
//
//		bool FlushBuffer		Is the stream buffer flushed after reading?
//				true			The part of the buffer that is already read, is freed
//				false			The data already read is still stored
//
//		long Size				Number of bytes read from the stream
//				-1, 0			Reads the whole stream buffer
//
//		long Position			Where to write into the file
//
//	NOTE:: FlushBuffer has no effect when STREAM_ACCUM_BUF flag is set
//
//	Returns the number of bytes read
//==============================================================================
long G3::ZIPSTREAM::DeflateToFile (const string &filename, long size, long position) {
    long fsize = 0;
	long chunkSize = size;
	long blankSize = 0;
	long ret = 0, i = 0;
	long written = 0;
	byte blank = 0;
	byte *data2 = NULL;

    Assert (bStream, "ZIPSTREAM::Open: bStream member is a NULL pointer..\n");
	Assert (!filename.empty (), "ZIPSTREAM::DeflateToFile: Filename argument is a NULL pointer");

    fsize = GetFileSize (filename);

    if (size <= 0)
        chunkSize = bStream->Tell (G3::STREAM::STREAM_END);

	if (position > fsize)
		blankSize = position - fsize;

	Assert (position >= 0, "ZIPSTREAM::DeflateToFile: Position argument is invalid");

	FILE *fo = fopen (filename.c_str (), "wb");
	if (!fo) {
		if (log)
			log->Report ("ERROR! ZIPSTREAM::DeflateToFile: Failed to open the file \"%s\"..\n", filename.c_str ());
		return 0;
	}

	fseek (fo, position, SEEK_SET);

	for (i=0; i<blankSize; i++)
		fwrite (&blank, blankSize, 1, fo);

    data2 = new byte [chunkSize + 1];

    ret = DeflateOut (data2, chunkSize);

    fwrite (data2, ret, 1, fo);

    delete [] data2;

	fclose (fo);

	return ret;
}
